//! SolMacroGen and MultiSolMacroGen
//!
//! This type encapsulates the logic for expansion of a Rust TokenStream from Solidity tokens. It
//! uses the `expand` method from `alloy_sol_macro_expander` underneath.
//!
//! It holds info such as `path` to the ABI file, `name` of the file and the rust binding being
//! generated, and lastly the `expansion` itself, i.e the Rust binding for the provided ABI.
//!
//! It contains methods to read the json abi, generate rust bindings from the abi and ultimately
//! write the bindings to a crate or modules.

use alloy_sol_macro_expander::expand::expand;
use alloy_sol_macro_input::{SolInput, SolInputKind};
use eyre::{Context, Ok, OptionExt, Result};
use foundry_common::fs;
use proc_macro2::{Span, TokenStream};
use std::{
    fmt::Write,
    path::{Path, PathBuf},
    str::FromStr,
};

pub struct SolMacroGen {
    pub path: PathBuf,
    pub name: String,
    pub expansion: Option<TokenStream>,
}

impl SolMacroGen {
    pub fn new(path: PathBuf, name: String) -> Self {
        Self { path, name, expansion: None }
    }

    pub fn get_sol_input(&self) -> Result<SolInput> {
        let path = self.path.to_string_lossy().into_owned();
        let name = proc_macro2::Ident::new(&self.name, Span::call_site());
        let tokens = quote::quote! {
            #name,
            #path
        };

        let sol_input: SolInput = syn::parse2(tokens).wrap_err("Failed to parse SolInput {e}")?;

        Ok(sol_input)
    }
}

pub struct MultiSolMacroGen {
    pub artifacts_path: PathBuf,
    pub instances: Vec<SolMacroGen>,
}

impl MultiSolMacroGen {
    pub fn new(artifacts_path: &Path, instances: Vec<SolMacroGen>) -> Self {
        Self { artifacts_path: artifacts_path.to_path_buf(), instances }
    }

    pub fn populate_expansion(&mut self, bindings_path: &Path) -> Result<()> {
        for instance in &mut self.instances {
            let path = bindings_path.join(format!("{}.rs", instance.name.to_lowercase()));
            let expansion = fs::read_to_string(path).wrap_err("Failed to read file")?;

            let tokens = TokenStream::from_str(&expansion)
                .map_err(|e| eyre::eyre!("Failed to parse TokenStream: {e}"))?;
            instance.expansion = Some(tokens);
        }
        Ok(())
    }

    pub fn generate_bindings(&mut self) -> Result<()> {
        for instance in &mut self.instances {
            let input = instance.get_sol_input()?.normalize_json()?;

            let SolInput { attrs: _attrs, path: _path, kind } = input;

            let tokens = match kind {
                SolInputKind::Sol(mut file) => {
                    let sol_attr: syn::Attribute = syn::parse_quote! {
                        #[sol(rpc, alloy_sol_types = alloy::sol_types, alloy_contract = alloy::contract)]
                    };
                    file.attrs.push(sol_attr);
                    expand(file).wrap_err("Failed to expand SolInput")?
                }
                _ => unreachable!(),
            };

            instance.expansion = Some(tokens);
        }

        Ok(())
    }

    pub fn write_to_crate(
        &mut self,
        name: &str,
        version: &str,
        bindings_path: &Path,
        single_file: bool,
        alloy_version: Option<String>,
    ) -> Result<()> {
        self.generate_bindings()?;

        let src = bindings_path.join("src");

        let _ = fs::create_dir_all(&src);

        // Write Cargo.toml
        let cargo_toml_path = bindings_path.join("Cargo.toml");
        let mut toml_contents = format!(
            r#"[package]
name = "{name}"
version = "{version}"
edition = "2021"

[dependencies]
"#
        );

        let alloy_dep = if let Some(alloy_version) = alloy_version {
            format!(
                r#"alloy = {{ git = "https://github.com/alloy-rs/alloy", rev = "{alloy_version}", features = ["sol-types", "contract"] }}"#
            )
        } else {
            r#"alloy = { git = "https://github.com/alloy-rs/alloy", features = ["sol-types", "contract"] }"#.to_string()
        };
        write!(toml_contents, "{alloy_dep}")?;

        fs::write(cargo_toml_path, toml_contents).wrap_err("Failed to write Cargo.toml")?;

        let mut lib_contents = String::new();
        write!(
            &mut lib_contents,
            r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
        //! This module contains the sol! generated bindings for solidity contracts.
        //! This is autogenerated code.
        //! Do not manually edit these files.
        //! These files may be overwritten by the codegen system at any time.
        "#
        )?;

        // Write src
        for instance in &self.instances {
            let name = instance.name.to_lowercase();
            let contents = instance.expansion.as_ref().unwrap().to_string();

            if !single_file {
                let path = src.join(format!("{name}.rs"));
                let file = syn::parse_file(&contents)?;
                let contents = prettyplease::unparse(&file);

                fs::write(path.clone(), contents).wrap_err("Failed to write file")?;
                writeln!(&mut lib_contents, "pub mod {name};")?;
            } else {
                write!(&mut lib_contents, "{contents}")?;
            }
        }

        let lib_path = src.join("lib.rs");
        let lib_file = syn::parse_file(&lib_contents)?;

        let lib_contents = prettyplease::unparse(&lib_file);

        fs::write(lib_path, lib_contents).wrap_err("Failed to write lib.rs")?;

        Ok(())
    }

    pub fn write_to_module(&mut self, bindings_path: &Path, single_file: bool) -> Result<()> {
        self.generate_bindings()?;

        let _ = fs::create_dir_all(bindings_path);

        let mut mod_contents = r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
        //! This module contains the sol! generated bindings for solidity contracts.
        //! This is autogenerated code.
        //! Do not manually edit these files.
        //! These files may be overwritten by the codegen system at any time.
        "#
        .to_string();

        for instance in &self.instances {
            let name = instance.name.to_lowercase();
            if !single_file {
                // Module
                write!(
                    mod_contents,
                    r#"pub mod {};
                "#,
                    instance.name.to_lowercase()
                )?;
                let mut contents = String::new();

                write!(contents, "{}", instance.expansion.as_ref().unwrap())?;
                let file = syn::parse_file(&contents)?;

                let contents = prettyplease::unparse(&file);
                fs::write(bindings_path.join(format!("{name}.rs")), contents)
                    .wrap_err("Failed to write file")?;
            } else {
                // Single File
                let mut contents = String::new();
                write!(contents, "{}\n\n", instance.expansion.as_ref().unwrap())?;
                write!(mod_contents, "{contents}")?;
            }
        }

        let mod_path = bindings_path.join("mod.rs");
        let mod_file = syn::parse_file(&mod_contents)?;
        let mod_contents = prettyplease::unparse(&mod_file);

        fs::write(mod_path, mod_contents).wrap_err("Failed to write mod.rs")?;

        Ok(())
    }

    /// Checks that the generated bindings are up to date with the latest version of
    /// `sol!`.
    ///
    /// Returns `Ok(())` if the generated bindings are up to date, otherwise it returns
    /// `Err(_)`.
    #[allow(clippy::too_many_arguments)]
    pub fn check_consistency(
        &self,
        name: &str,
        version: &str,
        crate_path: &Path,
        single_file: bool,
        check_cargo_toml: bool,
        is_mod: bool,
        alloy_version: Option<String>,
    ) -> Result<()> {
        if check_cargo_toml {
            self.check_cargo_toml(name, version, crate_path, alloy_version)?;
        }

        let mut super_contents = String::new();
        write!(
            &mut super_contents,
            r#"#![allow(unused_imports, clippy::all, rustdoc::all)]
            //! This module contains the sol! generated bindings for solidity contracts.
            //! This is autogenerated code.
            //! Do not manually edit these files.
            //! These files may be overwritten by the codegen system at any time.
            "#
        )?;
        if !single_file {
            for instance in &self.instances {
                let name = instance.name.to_lowercase();
                let path = if is_mod {
                    crate_path.join(format!("{name}.rs"))
                } else {
                    crate_path.join(format!("src/{name}.rs"))
                };
                let tokens = instance
                    .expansion
                    .as_ref()
                    .ok_or_eyre(format!("TokenStream for {path:?} does not exist"))?
                    .to_string();

                self.check_file_contents(&path, &tokens)?;

                write!(
                    &mut super_contents,
                    r#"pub mod {name};
                    "#
                )?;
            }

            let super_path =
                if is_mod { crate_path.join("mod.rs") } else { crate_path.join("src/lib.rs") };
            self.check_file_contents(&super_path, &super_contents)?;
        }

        Ok(())
    }

    fn check_file_contents(&self, file_path: &Path, expected_contents: &str) -> Result<()> {
        eyre::ensure!(
            file_path.is_file() && file_path.exists(),
            "{} is not a file",
            file_path.display()
        );
        let file_contents = &fs::read_to_string(file_path).wrap_err("Failed to read file")?;

        // Format both
        let file_contents = syn::parse_file(file_contents)?;
        let formatted_file = prettyplease::unparse(&file_contents);

        let expected_contents = syn::parse_file(expected_contents)?;
        let formatted_exp = prettyplease::unparse(&expected_contents);

        eyre::ensure!(
            formatted_file == formatted_exp,
            "File contents do not match expected contents for {file_path:?}"
        );
        Ok(())
    }

    fn check_cargo_toml(
        &self,
        name: &str,
        version: &str,
        crate_path: &Path,
        alloy_version: Option<String>,
    ) -> Result<()> {
        eyre::ensure!(crate_path.is_dir(), "Crate path must be a directory");

        let cargo_toml_path = crate_path.join("Cargo.toml");

        eyre::ensure!(cargo_toml_path.is_file(), "Cargo.toml must exist");
        let cargo_toml_contents =
            fs::read_to_string(cargo_toml_path).wrap_err("Failed to read Cargo.toml")?;

        let name_check = format!("name = \"{name}\"");
        let version_check = format!("version = \"{version}\"");
        let alloy_dep_check = if let Some(version) = alloy_version {
            format!(
                r#"alloy = {{ git = "https://github.com/alloy-rs/alloy", rev = "{version}", features = ["sol-types", "contract"] }}"#,
            )
        } else {
            r#"alloy = { git = "https://github.com/alloy-rs/alloy", features = ["sol-types", "contract"] }"#.to_string()
        };
        let toml_consistent = cargo_toml_contents.contains(&name_check) &&
            cargo_toml_contents.contains(&version_check) &&
            cargo_toml_contents.contains(&alloy_dep_check);
        eyre::ensure!(
            toml_consistent,
            r#"The contents of Cargo.toml do not match the expected output of the latest `sol!` version.
                This indicates that the existing bindings are outdated and need to be generated again."#
        );

        Ok(())
    }
}
